from Tkinter import *
from tkFont import Font
from contextlib import contextmanager
from functools import partial

from nipponsteel.gui.player.sprites import FloorSprite, PlaceSprite, CraneSprite, MoveSprite
from nipponsteel.gui.player.layers import LayerManager
from nipponsteel.gui.utils import Rect, Tooltip
from nipponsteel.utils import Namespace
from nipponsteel import Stack, Crane

        
class Player(LabelFrame):
    frame_rate = 40.0
    min_speed  = 2**-3
    max_speed  = 2**10
    min_scale  = 1.0
    max_scale  = 16.0
    min_stack_size = 0.5
    max_stack_size = 10.0
    
    def __init__(self, master, width=800, height=600):
        LabelFrame.__init__(self, master)
        self.tape = None
        self.sprite = dict()
        self.pixel_ratio = None
        self.speed = 1.0
        self.scale = 1.0
        self.stack_size = 3.0
        self.width = width
        self.height = height
        self.previous_move_sprite = None
        
        # Create child widgets and set up layout ----------
        self.rowconfigure(0, weight=1)
        self.columnconfigure(0, weight=1)
        
##        self.info = Tooltip(self, delay=None)
        self.canvas = Canvas(self, width=self.width, height=self.height, borderwidth=0)
        self.canvas.grid(row=0, column=0, sticky=NE+SW)
        self.layers = LayerManager(self.canvas)
        self.yscrollbar = Scrollbar(self, orient=VERTICAL, command=self.canvas.yview)
        self.yscrollbar.grid(row=0, column=1, sticky=N+S)
        self.xscrollbar = Scrollbar(self, orient=HORIZONTAL, command=self.canvas.xview)
        self.xscrollbar.grid(row=1, column=0, sticky=E+W)
        self.canvas.configure(xscrollcommand=self.xscrollbar.set, 
                              yscrollcommand=self.yscrollbar.set)
        # Bind events -------------------------------------
        self.bind("<Configure>", self.update_size)
##        self.canvas.bind("<Enter>", self.watch_mouse)
##        self.canvas.bind("<Leave>", self.ignore_mouse)
##        self.canvas.bind("<Motion>", self.update_mouse)
        
    def setup(self, tape):
        """Sets up (but doesn't draw) the sprites used for showing an animation of the tape."""
        if self.tape is not None:
            self.teardown()
        self.tape = tape
        
        # the dimensions of the warehouse will be used to compute the pixel ratio
        warehouse = self.tape.warehouse
        warehouse_rect = Rect(*[place.meta.coords for place in warehouse.places.itervalues()])
        warehouse_rect.x0 -= self.stack_size
        warehouse_rect.x1 += self.stack_size
        warehouse_rect.y0 -= self.stack_size
        warehouse_rect.y1 += self.stack_size
        warehouse.meta.coords = warehouse_rect
        
        # create sprites
        self.sprite = dict()
        self.sprite["floor"] = FloorSprite(self, warehouse.meta.coords)
        for place in warehouse.places.itervalues():
            sprite = PlaceSprite(self, place)
            sprite.set_layer(1)
            self.sprite[place] = sprite
        for crane in warehouse.handlers.itervalues():
            sprite = CraneSprite(self, crane)
            sprite.set_layer(3)
            self.sprite[crane] = sprite
            
        # compute pixel ratio, update the label, and draw everything
        self.update_pixel_ratio(redraw=False)
        self.update_label()
        self.draw()
        
    def teardown(self):
        """Clears the player and discards any loaded tape."""
        if self.tape is None:
            return
        self.clear()
        self.previous_move_sprite = None
        self.tape = None
        self.sprite = dict()
        self.pixel_ratio = None
        self.update_label()
        self.update_scrollregion()
        self.canvas.tag_unbind(ALL, "<Any>")
        
    def update_pixel_ratio(self, redraw=True):
        """The pixel ratio is a coefficient which is multiplied by the scale to give the actual 
        scaling factor of distance units to pixels in the canvas. The pixel ratio is the number 
        of pixels per unit of distance, computed such that, using a 1x scale, the whole warehouse 
        is drawn on the canvas."""
        if self.tape is not None:
            old_pixel_ratio = self.pixel_ratio
            coords = self.tape.warehouse.meta.coords
            h_pixel_ratio = self.width  / float(coords.width)
            v_pixel_ratio = self.height / float(coords.height)
            self.pixel_ratio = min(h_pixel_ratio, v_pixel_ratio)
            print "pixel ratio:", self.pixel_ratio
            if redraw and self.pixel_ratio != old_pixel_ratio:
                self.redraw()
                
    def update_scrollregion(self):
        """Updates the scroll region to include all objects in the canvas. If the bounding box is 
        smaller than the size of the canvas, the scroll region is expanded to the size of the 
        canvas and centered with the warehouse."""
        if self.tape is None:
            left, top, right, bottom = (0, 0, 0, 0)
        else:
            left, top, right, bottom = [int(self.scaled(v)) 
                                        for v in self.tape.warehouse.meta.coords]
            if right - left < self.width:
                delta  = self.width - (right - left)
                left  -= delta / 2
                right += delta / 2
            if bottom - top < self.height:
                delta   = self.height - (bottom - top)
                top    -= delta / 2
                bottom += delta / 2
            if self.scale == 1.0:
                assert abs(self.width  - (right  - left)) <= 2
                assert abs(self.height - (bottom - top))  <= 2
        self.canvas.configure(scrollregion=(left, top, right, bottom))
        
    def update_size(self, event=None):
        """Updates the dimensions of the player and recomputes the pixel ratio. If the pixel ratio 
        is changed, the warehouse is redrawn using the new ratio, which means that the player will
        adapt to the new size of the window."""
        self.update_idletasks()
        self.width  = self.canvas.winfo_width()
        self.height = self.canvas.winfo_height()
        old_pixel_ratio = self.pixel_ratio
        self.update_pixel_ratio()
        if self.pixel_ratio == old_pixel_ratio:
            self.update_scrollregion()
            
    def update_label(self):
        """Updates the label that appears at the top of the Player."""
        if self.tape is None:
            self.configure(text="")
        else:
            self.configure(text="%s (speed %sx, zoom %sx)" % 
                           (self.tape.meta.filename, self.speed, self.scale))
            
    # -------------------------------------------
    @contextmanager
    def cleared(self):
        self.clear()
        yield
        self.draw()
        
    def clear(self):
        for sprite in self.sprite.itervalues():
            sprite.clear()
        if self.previous_move_sprite is not None:
            self.previous_move_sprite.clear()
            
    def draw(self):
        if self.tape is not None:
            for sprite in self.sprite.itervalues():
                sprite.draw()
            if self.previous_move_sprite is not None:
                self.previous_move_sprite.draw()
            self.update_scrollregion()
            
    def redraw(self):
        self.clear()
        self.draw()
        
    def display(self, move, at_end=None):
        place = self.tape.warehouse.places[move.place]
        crane = self.tape.warehouse.handlers[move.handler]
        
        if self.previous_move_sprite is not None:
            self.previous_move_sprite.clear()
        move_sprite = MoveSprite(self, move)
        move_sprite.set_layer(2)
        move_sprite.draw()
        place_sprite = self.sprite[place]
        crane_sprite = self.sprite[crane]
        callback = partial(self._finalize_move, crane_sprite, place_sprite, at_end)
        crane_sprite.move_to(*place.meta.coords, duration=1.0 / self.speed, at_end=callback)
        self.previous_move_sprite = move_sprite
        
    def _finalize_move(self, crane_sprite, place_sprite, at_end):
        crane_sprite.redraw()
        if place_sprite.place.is_buffer:
            place_sprite.redraw()
        if at_end is not None:
            at_end()
            
    # -------------------------------------------
    def set_speed(self, speed):
        self.speed = max(self.min_speed, min(self.max_speed, speed))
        self.update_label()
        
    def set_scale(self, scale):
        new_scale = max(self.min_scale, min(self.max_scale, scale))
        if new_scale == self.scale:
            return
        with self.cleared():
            self.scale = new_scale
            self.update_label()
            if self.previous_move_sprite is not None:
                self.previous_move_sprite.redraw()
                
    def set_stack_size(self, stack_size):
        new_stack_size = max(self.min_stack_size, min(self.max_stack_size, stack_size))
        if new_stack_size == self.stack_size:
            return
        with self.cleared():
            self.stack_size = new_stack_size
            
    def scaled(self, value):
        """Converts a value in distance units (meters for instance) to pixels in the canvas, 
        taking into account the dimensions of the warehouse (pixel ratio) and the scale currently 
        in use. The pixel ratio is the number of pixels per unit of distance which corresponds to 
        drawing the whole warehouse occupying the whole canvas with a scale of 1."""
        return value * self.scale * self.pixel_ratio
        
    @property
    def frame_time(self):
        """Interframe time in miliseconds."""
        return int(1000.0 / self.frame_rate)
        